Skip to main content

05 - 选型与落地

前置:不需要。本篇是全专题结论,可独立阅读,细节按需回查前四篇。

本篇回答:四条路线该选哪条、重放会怎么污染 trace、以及什么都不引入时最小可用方案怎么搭。

本篇会用到的词

意思
幂等键让重跑变得无害的那个确定性字符串。判断树里唯一一个「无论选哪条路线都必须自己做」的东西
无人值守任务跑起来之后没有人盯着,也没有用户会回头来看结果。这是 LangGraph 那条路线的分界线 —— 它的检查点需要有人主动来捡
跨服务编排一个任务的步骤分散在多个服务里,需要统一的状态和重试。这是选 Temporal 而不是更轻方案的主要理由
重放污染重放式方案恢复时会让已完成的步骤再次经过埋点代码,于是 trace 里出现重复 span、按 span 累加的成本虚高
ActivityTemporal 里包裹「真正干活」那部分代码的单元。重放时它不执行,所以埋点应该放在这里而不是工作流函数里

一、四条路线定位

路线代表状态形式自动恢复确定性约束额外基础设施
框架内置LangGraph Checkpointer快照
数据库即状态DBOS步骤输出
单独运行时Restate执行日志一个进程
工作流引擎Temporal事件历史一个集群

二、选型判据

先把幂等键做掉这一步跳不过去做完之后,直接跳到第三个问题有不可撤销的外部副作用吗付款 · 发邮件 · 下单没有单次重跑成本超过 1 分钱吗超过不超过是无人值守的后台任务吗不是要跨服务编排或挂起超过一天吗什么框架都不用上挂了直接重跑LangGraph用户下次交互时自然恢复TemporalDBOS / Restate不要整棵树里最重要的是左上那一格:框架只保证「这一步至少执行一次」,「恰好生效一次」取决于你调的下游服务认不认去重键。顺序反了,你会得到一个能可靠地把邮件发两遍的系统。
先把幂等键做掉,再去纠结选哪个框架 —— 这个顺序不能反。有不可撤销副作用的任务甚至不必回答第二个问题:成本再低也不能重发。

红色节点是这棵树上唯一的强制项。01 篇 3.1 节已经论证过:崩溃窗口无法消除 → 某些步骤必然重跑 → 幂等键是唯一出路。

顺序反了的后果:得到一个能可靠地把邮件发两遍的系统。

三、重放对可观测性的污染

这是持久化执行与 Agent 可观测性的交叉点,四篇里都提到过,此处集中说明。

3.1 现象

重放式方案(Temporal / DBOS / Restate)在恢复时会把工作流代码从头重跑。若在工作流函数里直接埋点,一个被重放三次的工作流会产生三份重复 span。

重放式方案的一个副作用:trace 里会出现重复的 span首次执行步骤 1-17 各产生一个 span这些是真实发生的崩溃恢复重放步骤 1-17 再次经过埋点代码但并不真正执行步骤 18 起才是真正执行trace 里前 17 步重复出现,且耗时都极短按 span 累加成本的话,账单会虚高解法是把埋点放进 Activity 而不是 Workflow:Activity 只在真正执行时才跑,重放时不执行,天然不会重复。
这也是一个判断可观测性方案成熟度的问题 —— 问供应商「你们怎么处理持久化执行的重放」,能答上来的说明真在生产环境里跑过。

3.2 三条处理原则

原则做法
埋点放在被记录的步骤里Activity / step / run() 内部只在真正执行时运行,重放时不执行,天然不重复
成本以实际调用为准最可靠的口径是网关侧记录的真实请求 —— 重放不产生网关请求。见 可观测性 03 篇
必须在工作流层埋点时,带重放标记各家 SDK 都提供"当前是否处于重放中"的判断,用它标记或跳过
# 若一定要在工作流函数里记录,先判断是否处于重放
if not workflow.unsafe.is_replaying():
logger.info("进入第二阶段") # 只在首次真实执行时记录

这也是评估可观测性方案的一个探针:问供应商如何处理持久化执行的重放,能答上来说明其在生产环境跑过。

四、可自行实现的最小方案

尚未引入任何框架时,下面四步能覆盖大部分需求:

# ① 为每个任务分配 ID,每个步骤生成确定性的键
# 确定性是关键 —— 重跑时必须算出完全相同的值,不能用 uuid4() 或时间戳
step_key = f"{task_id}:{step_index}"

# ② 每步执行前先查、执行后再写
result = store.get(step_key)
if result is None:
result = run_step(...)
store.put(step_key, result)

# ③ 所有外部副作用带上同一个键做去重
send_email(..., idempotency_key=step_key)

# ④ 定时任务扫描超时未完成的 task_id 并重新拉起。
# 拉起前必须取分布式锁,否则多实例会重复执行同一任务
# —— 即 04 篇 1.3 节 DBOS 用 Conductor 解决的问题,
# 自行实现时这是最常见的遗漏点。
if lock.acquire(f"recover:{task_id}", ttl=300):
resume(task_id)

这套方案缺少的是:跨服务编排、挂起不占资源、自动重试策略、可视化。当这几项开始成为痛点时再选框架 —— 那时你已经理解了这些框架在解决什么,选型会准确得多。

五、检查点数据的安全边界

这一点在其他资料里很少被提及,但影响不小。

一个运行三个月的 Agent 平台,检查点表里累积着:所有用户的完整对话、上传文档的内容、工具返回的内部数据。

这张表的敏感级别不低于用户表,但它通常没有对应的:

缺失项后果
访问控制任何能读该库的服务都能读到全部对话内容
字段级加密数据库备份泄露等同于对话内容泄露
保留期策略无限期留存,合规问询时无法回答
脱敏用户粘贴的身份证号、密钥被原样存下

02 篇 3.2 节提到的 prune / delete_thread 不只是省存储 —— 它们是数据保留合规的执行点

六、与本板块其他专题的关系

专题交叉点
网关网关重试处理单次调用,持久化处理整个任务。叠加使用:网关把单次失败率压到 0.01%,其余由持久化兜住
安全检查点是高价值数据资产,也是高价值攻击目标 —— 见第五节
可观测性重放污染 —— 见第三节

七、结论

持久化执行的本质是:承认失败必然发生,把"失败之后怎么办"从临场处理变成设计期决策。

四条路线的差别只是这个决策落在哪一层 —— 框架内、数据库里、运行时里,还是一个独立集群里。

无论选哪条,有两件事始终是使用方自己的责任:幂等键,以及检查点里那份数据的安全

← 回到 专题索引  ·  Agent Infra 板块总览